perf: cache MatrixAccessor properties to avoid redundant recomputation#616
perf: cache MatrixAccessor properties to avoid redundant recomputation#616MaykThewessen wants to merge 3 commits intoPyPSA:masterfrom
Conversation
Covers the code paths optimised by these PRs: - PyPSA#616 cached_property on MatrixAccessor (flat_vars / flat_cons) - PyPSA#617 np.char.add for label string concatenation - PyPSA#618 sparse matrix slicing in MatrixAccessor.A - PyPSA#619 numpy solution unpacking Reproduces benchmark results on PyPSA SciGrid-DE (24–500 snapshots) and a synthetic model. Supports JSON output and --compare mode for cross-branch comparison. Reproduce with: python benchmark/scripts/benchmark_matrix_gen.py -o results.json --label "after" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Added Reproduce with: python benchmark/scripts/benchmark_matrix_gen.py -o results.json --label "with-PR-616"
python benchmark/scripts/benchmark_matrix_gen.py --compare before.json after.jsonThe benchmark times |
3373a1b to
bc3b49e
Compare
Adds benchmark/scripts/benchmark_matrix_gen.py covering all four performance code paths: - PyPSA#616 cached_property on MatrixAccessor (flat_vars / flat_cons) - PyPSA#617 np.char.add label string concatenation - PyPSA#618 single-step sparse matrix slicing - PyPSA#619 numpy dense-array solution unpacking Reproduce with: python benchmark/scripts/benchmark_matrix_gen.py -o results.json python benchmark/scripts/benchmark_matrix_gen.py --include-solve # PR PyPSA#619 python benchmark/scripts/benchmark_matrix_gen.py --compare before.json after.json Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds benchmark/scripts/benchmark_matrix_gen.py covering all four performance code paths: - PyPSA#616 cached_property on MatrixAccessor (flat_vars / flat_cons) - PyPSA#617 np.char.add label string concatenation - PyPSA#618 single-step sparse matrix slicing - PyPSA#619 numpy dense-array solution unpacking Reproduce with: python benchmark/scripts/benchmark_matrix_gen.py -o results.json python benchmark/scripts/benchmark_matrix_gen.py --include-solve # PR PyPSA#619 python benchmark/scripts/benchmark_matrix_gen.py --compare before.json after.json Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
f499edd to
e639bb6
Compare
Adds benchmark/scripts/benchmark_matrix_gen.py covering all four performance code paths: - PyPSA#616 cached_property on MatrixAccessor (flat_vars / flat_cons) - PyPSA#617 np.char.add label string concatenation - PyPSA#618 single-step sparse matrix slicing - PyPSA#619 numpy dense-array solution unpacking Reproduce with: python benchmark/scripts/benchmark_matrix_gen.py -o results.json python benchmark/scripts/benchmark_matrix_gen.py --include-solve # PR PyPSA#619 path python benchmark/scripts/benchmark_matrix_gen.py --compare before.json after.json Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds benchmark/scripts/benchmark_matrix_gen.py covering all four performance code paths: - PyPSA#616 cached_property on MatrixAccessor (flat_vars / flat_cons) - PyPSA#617 np.char.add label string concatenation - PyPSA#618 single-step sparse matrix slicing - PyPSA#619 numpy dense-array solution unpacking Reproduce with: python benchmark/scripts/benchmark_matrix_gen.py -o results.json python benchmark/scripts/benchmark_matrix_gen.py --include-solve # PR PyPSA#619 python benchmark/scripts/benchmark_matrix_gen.py --compare before.json after.json Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Converts vlabels, vtypes, lb, ub, clabels, A, sense, b, c, Q from @Property to @cached_property, and extends clean_cached_properties() to clear all of them. Avoids recomputing expensive matrix operations (e.g. flat DataFrame flattening, sparse matrix slicing) on each access.
Adds benchmark/scripts/benchmark_matrix_gen.py covering all four performance code paths: - PyPSA#616 cached_property on MatrixAccessor (flat_vars / flat_cons) - PyPSA#617 np.char.add label string concatenation - PyPSA#618 single-step sparse matrix slicing - PyPSA#619 numpy dense-array solution unpacking Reproduce with: python benchmark/scripts/benchmark_matrix_gen.py -o results.json python benchmark/scripts/benchmark_matrix_gen.py --include-solve # PR PyPSA#619 python benchmark/scripts/benchmark_matrix_gen.py --compare before.json after.json Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
9f21863 to
4985f9a
Compare
Adds benchmark/scripts/benchmark_matrix_gen.py covering all four performance code paths: - PyPSA#616 cached_property on MatrixAccessor (flat_vars / flat_cons) - PyPSA#617 np.char.add label string concatenation - PyPSA#618 single-step sparse matrix slicing - PyPSA#619 numpy dense-array solution unpacking Reproduce with: python benchmark/scripts/benchmark_matrix_gen.py -o results.json python benchmark/scripts/benchmark_matrix_gen.py --include-solve # PR PyPSA#619 python benchmark/scripts/benchmark_matrix_gen.py --compare before.json after.json Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
for more information, see https://pre-commit.ci
|
@MaykThewessen Can you provide the results you got with the benchmarks? Compare master vs feature branch. |
|
@MaykThewessen THis PR in general looks good. We need to take care with cache cleanup though. Ill look into it. |
|
If you want you can share your thoughts on #622, which will hopefully cover your workflow even better. |
Benchmark Results: master vs PR #616Tested on actual linopy implementation (not mocked) using PyPSA SciGrid-DE at multiple problem sizes. Setup: Python 3.14.3, numpy 2.4.3, Apple M-series (arm64), macOS, 5 repeats (best-of).
Summary: Consistent 1.1–1.8x speedup across matrix generation phases, with larger improvements at medium problem sizes (100–200 snapshots). The caching benefit is most visible on Benchmark methodology
|
|
@FBumann Thanks for reviewing! Happy to help with the cache cleanup — let me know if you'd like me to look at specific scenarios where the cache needs invalidation, or if you'd prefer to handle it yourself. |
|
@MaykThewessen Thinking about those scenarios is the actual work. So if you can, a list of such would help. The actual implementation is easy then. |
|
@MaykThewessen As is said in #619, your Benchmark does not really isolate the Content if this PR. And its not properly measuring the caching improvement, as you call clear_cached_properties in places where you should not... I can merge this anyway as im convinced that its an improvement, but please take your time and check that your benchmark actually measures what its supposed to when publishing results next time. |
|
You're right about the benchmark — calling On cache invalidation scenarios:
The first two (add/remove variables/constraints) are the critical ones since they change the shape of the matrix. Bounds/RHS/objective modifications only change values within an existing structure. One approach: blanket |
Summary
vlabels,clabels,A,c,b,sense,lb,ub,vtypes, andQfrom@propertyto@cached_propertyinMatrixAccessorclean_cached_properties()for proper invalidationMotivation
During
to_highspy()(and other direct API exporters),MatrixAccessorproperties are accessed multiple times — both directly and indirectly. For example,M.Ainternally accessesM.clabelsandM.vlabels, andM.caccessesM.flat_vars. Each access recomputes from scratch, including rebuilding sparse matrices and flattening DataFrames.For large models (~593K variables, ~1.38M constraints), this redundant recomputation adds measurable overhead per solve call. Caching eliminates this since the underlying model data doesn't change between property accesses within a single solve.
The cache is already properly invalidated at the start of
Model.solve()andModel._mock_solve()via existingclean_cached_properties()calls.Context
See discussion in #198 (comment) for profiling data from a real-world 52-chunk DC-OPF optimization.
Test plan
test_matrices.pytests pass (verify shapes, values, masked models)test_matrices_properties_are_cachedverifies identity caching and invalidationtest_optimization.pyhighs-direct tests pass (24/25 — one pre-existing failure intest_modified_model)test_io.pytests passNote:
test_modified_modelfails on upstreammasteras well — it's a pre-existing issue unrelated to this change.🤖 Generated with Claude Code